﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Passwords.Models;
using System.Collections.ObjectModel;
using Passwords.ViewModels;
using Passwords.DAL;

namespace Passwords.Utils
{
    public static class DataUtil
    {

        /// <summary>
        /// Finds a category in a list by it's id.
        /// </summary>
        /// <param name="list">list of categories.</param>
        /// <param name="id">id of the category to find</param>
        /// <returns>category with the specified id, otherwise null.</returns>
        public static CategoryContract FindCategory(this IList<CategoryContract> list, string id)
        {
            return list.FirstOrDefault(c => c.Id == id);
        }

        /// <summary>
        /// Reorders a password in a given collection of PasswordContract.
        /// </summary>
        /// <param name="collection">observable collection wich contains the specified password.</param>
        /// <param name="password">PasswordContract in the collection to reindex in order of it's name.</param>
        /// <param name="originalName">The original name of the password when it was sorted into the collection.</param>
        public static void Reorder(this ObservableCollection<PasswordContract> collection, PasswordContract password, string originalName)
        {
            if (password == null) throw new ArgumentNullException("password");
            if (collection == null) throw new ArgumentNullException("collection");

            if (originalName != password.Name)
            {
                int origIndex = collection.IndexOf(password);

                PasswordComparer comparer = new PasswordComparer { Password = password, CompareName = originalName ?? string.Empty };

                int index = collection.BinarySearch(password, comparer);
                if (index < 0) index = ~index;

                if (index > 0 && index > origIndex) index--;
                if (origIndex != index)
                {
                    collection.Move(origIndex, index);
                }
            }
        }


        /// <summary>
        /// Sorts a password into a list of passwords alphabetically sorted by name.
        /// </summary>
        /// <param name="list">the list of passwords to add.</param>
        /// <param name="password">password to add.</param>
        public static void SortInto(this IList<PasswordContract> list, PasswordContract password)
        {
            var comparer = new PasswordComparer { Password = password };

            int index = list.BinarySearch(password, comparer);
            if (index < 0) index = ~index;
            list.Insert(index, password);
        }

        /// <summary>
        /// Reorders a category in a given collection of CategoryContract.
        /// </summary>
        /// <param name="collection">observable collection wich contains the specified category.</param>
        /// <param name="category">CategoryContract in the collection to reindex in order of it's name.</param>
        /// <param name="originalName">The original name of the category when it was sorted into the collection.</param>
        public static void Reorder(this ObservableCollection<CategoryContract> collection, CategoryContract category, string originalName)
        {
            if (category == null) throw new ArgumentNullException("category");
            if (collection == null) throw new ArgumentNullException("collection");

            if (originalName != category.Name)
            {
                int origIndex = collection.IndexOf(category);

                CategoryComparer comparer = new CategoryComparer { Category = category, CompareName = originalName };

                int index = collection.BinarySearch(category, comparer);
                if (index < 0) index = ~index;

                if (index > 0 && index > origIndex) index--;
                if (origIndex != index)
                {
                    collection.Move(origIndex, index);
                }
            }
        }


        /// <summary>
        /// Sorts a password into a list of categories alphabetically sorted by name.
        /// </summary>
        /// <param name="list">the list of categories to add.</param>
        /// <param name="password">category to add.</param>
        public static void SortInto(this IList<CategoryContract> list, CategoryContract category)
        {
            CategoryComparer comparer = new CategoryComparer { Category = category };

            int index = list.BinarySearch(category, comparer);
            if (index < 0) index = ~index;
            list.Insert(index, category);
        }


        public static int BinarySearch<T>(this IList<T> list, T value, IComparer<T> comparer)
        {
            //if (comparer == null) comparer = Comparer<T>.Default;
            int hi = list.Count - 1;
            int low = 0;
            int n = hi / 2;

            while (low <= hi)
            {
                int median = GetMedian(low, hi);
                int num = comparer.Compare(list[median], value);
                if (num == 0) return median;
                if (num < 0)
                {
                    low = median + 1;
                }
                else
                {
                    hi = median - 1;
                }
            }
            return ~low;
        }

        private static int GetMedian(int low, int hi)
        {
            return (low + ((hi - low) >> 1));
        }

        public static void CopyFrom(this PasswordContract dest, PasswordContract source)
        {
            if (null == dest) throw new ArgumentNullException("dest");
            if (null == source) throw new ArgumentNullException("source");
           // if (null == viewModel) throw new ArgumentNullException("viewModel");

            //if (dest.Name != source.Name)
            //{
            //    dest.Name = source.Name;
            //    viewModel.Passwords.Reorder(dest, source.Name);
            //}

            dest.Name = source.Name;
            dest.IsFavorite = source.IsFavorite;
            dest.IsSynced = source.IsSynced;
            dest.TemplateId = source.TemplateId;
            dest.Modified = source.Modified;



            // Note: 
            // the following code is not necassary, as CopyFrom is only called when to undo changes made on a password which only happens in edit view, where password.Categories remains
            // untouched unless Command.SavePassword is executed, which is mutually exclusive with Command.UndoPassword which calls this method.
            //
            //var copyIds = copy.CategoryIds;
            //UpdateCategories(original, viewModel, copyIds);

            dest.CategoryIds = source.CategoryIds;
            dest.IsModified = source.IsModified;

        }

        /// <summary>
        /// Updates the categories of a password by replacing it with a specified array of category ids.
        /// This method updates all collections which track a password<->category link, which is Categories.Passwords and Password.Categories.
        /// </summary>
        /// <param name="password">Password to update.</param>
        /// <param name="viewModel">ViewModel to receive additional data.</param>
        /// <param name="newCategoryIds">array of category ids to use for the specified password.</param>
        public static void UpdateCategories(PasswordContract password, DataViewModel viewModel, string[] newCategoryIds)
        {
            if (null == password) throw new ArgumentNullException("password");
            if (null == viewModel) throw new ArgumentNullException("viewModel");

            var categoryIds = password.Categories.Select(c => c.Id).ToArray();
            var categoriesToAdd = newCategoryIds.Except(categoryIds).ToArray();
            var categoriesToRemove = categoryIds.Except(newCategoryIds).ToArray();
            if (categoriesToAdd.Length > 0 || categoriesToRemove.Length > 0)
            {
                foreach (CategoryContract category in viewModel.Categories)
                {
                    if (categoriesToRemove.Any(id => id == category.Id))
                    {
                        category.Passwords.Remove(password);
                        password.Categories.Remove(category);
                    }
                    if (categoriesToAdd.Any(id => id == category.Id))
                    {
                        category.Passwords.SortInto(password);
                        password.Categories.Add(category);
                    }
                }
            }
        }


        public static ObservableChildCollection<FieldContract> GetEditFields(this PasswordContract password, DataViewModel viewModel)
        {
            if (null == password) throw new ArgumentNullException("password");
            if (null == viewModel) throw new ArgumentNullException("viewModel");
            Dictionary<string, FieldContract> passwordFields = password.Fields.ToDictionary(f => f.Id, f => f);

            ObservableChildCollection<FieldContract> result = new ObservableChildCollection<FieldContract>(password);

            List<FieldContract> map = password.Fields.ToList();
            CategoryContract category = FindCategory(viewModel.Categories, password.TemplateId);
            if (category != null)
            {
                foreach (FieldContract field in category.Fields)
                {
                    field.IsCustom = false;
                    // create a clone to ensure that the original data (either a template from a category, or the current itme)
                    // remains untouched while editing, until the changes are accepted:
                    var orig = password.FindField(field.Id);
                    if (orig != null) map.Remove(orig);
                    FieldContract add = orig ?? field.Clone();
                    add.Name = field.Name;
                    result.Add(add);
                }

            }
            // now add the fields from the password which where not found in the category's field list:
            foreach (FieldContract field in map)
            {
                field.IsCustom = true;
                result.Add(field);
            }
            map.Clear();
            return result;
        }

        public static void UpdateFields(this PasswordContract password, IEnumerable<FieldContract> fields)
        {
            if (null == password) throw new ArgumentNullException("password");

            password.Fields.Clear();
            if (fields != null)
            {
                foreach (FieldContract field in fields)
                {
                    if (!string.IsNullOrEmpty(field.Value))
                    {
                        password.Fields.Add(field);
                    }
                }
            }
        }

        public static FieldContract FindField(this PasswordContract password, string id)
        {
            return password.Fields.FirstOrDefault(d => d.Id.Equals(id, StringComparison.InvariantCultureIgnoreCase));
        }


        public static void UndoCategories(this DataViewModel viewModel)
        {
            foreach (CategoryContract c in viewModel.Categories.ToArray())
            {
                UndoCategory(c, viewModel);
            }
        }

        /// <summary>
        /// Saves a category.
        /// </summary>
        /// <param name="category">Category to save</param>
        /// <param name="viewModel">ViewModel to access and modifiy metadata.</param>
        public static void SaveCategory(this CategoryContract category, DataViewModel viewModel, string originalName)
        {
            category.TransferFields = category.Fields.ToArray();
            category.ViewModel = viewModel;
            if (!viewModel.Categories.Contains(category))
            {
                viewModel.Categories.SortInto(category);
            }
            else
            {
                viewModel.Categories.Reorder(category, originalName);
            }
            viewModel.Context.SaveCategory(category);

            bool isInTemplates = viewModel.Templates.Contains(category);
            bool isTemplate = category.Fields.Count > 0;
            if (isInTemplates != isTemplate)
            {
                if (isTemplate) viewModel.Templates.SortInto(category);
                else viewModel.Templates.Remove(category);
            }
            else
            {
                if (isTemplate) viewModel.Templates.Reorder(category, originalName);
            }
        }


        public static void UndoCategory(this CategoryContract category, DataViewModel viewModel)
        {
            if (string.IsNullOrEmpty(category.Id))
            {
                DataUtil.DeleteCategory(category, viewModel);
            }
            else
            {
                DataContext context = viewModel.Context;
                if (string.IsNullOrEmpty(category.Id))
                {
                    viewModel.Categories.Remove(category);
                    viewModel.EditCategory = null;
                }
                else if (category.IsModified)
                {
                    CategoryContract src = viewModel.ReadCategory(category.Id);
                    if (src != null) src.CopyTo(category);
                }
            }
        }



        public static void CopyTo(this CategoryContract src, CategoryContract dst)
        {
            dst.Modified = src.Modified;
            dst.Name = src.Name;

            dst.Fields.Clear();
            foreach (var c in src.Fields) dst.Fields.Add(c);
            dst.IsModified = false;
        }


        public static void DeleteCategory(this CategoryContract category, DataViewModel viewModel)
        {
            if (category == null) throw new ArgumentNullException("category");
            if (viewModel == null) throw new ArgumentNullException("viewModel");

            if (category.Passwords.Count == 0)
            {
                string id = category.Id;
                if (string.IsNullOrEmpty(id) || viewModel.Context.DeleteCategory(id))
                {
                    category.ViewModel = null;
                    viewModel.Categories.Remove(category);
                    viewModel.Templates.Remove(category);
                }
            }
        }

        public static FieldContract Clone(this FieldContract src)
        {
            FieldContract copy = new FieldContract
            {
                Id = src.Id,
                ViewModel = src.ViewModel,
                FieldType = src.FieldType,
                Name = src.Name,
                MinChars = src.MinChars,
                MaxChars = src.MaxChars,
                Value = src.Value,
                IsModified = src.IsModified,
            };
            return copy;
        }
    }
}
